self.importScripts("fs.js")

const isLittleEndian = true;


const EventRecordDownloadCompleted = 0x00000000;

/****frames come from websocket */
const FRAME_CONFIG              = 0x68643030;
const FRAME_IDR                 = 0x62643030;
const FRAME_PARTIAL             = 0x63643030;
const FRAME_AUDIO               = 0x62773030;
const FRAME_GPS                 = 0x73706730;
const FRAME_END                 = 0x481;
//I partial frame 
const FRAME_PARTIAL_IDR         = 0x62643130
//P partial frame
const FRAME_PARTIAL_P           = 0x63643130


const VideoFrameLast            = -1;
const VideoFrameFirst           = 0;
const VideoFrameMiddle          = 1;


/***send data to decoder.js */
const REQUEST_NEW_DECODER        = 0x6001;
const REQUEST_FEED_DECODER_DATA  = 0x6003;
const REQUEST_FEED_GPS_DATA      = 0x6005;

/****Request */
const REQUEST_FETCH_START        = 0x6001;
const REQUEST_FETCH_RELEASE      = 0x6003;
const REQUEST_FETCH_FRAMES       = 0x6004;
const REQUEST_FETCH_PASSWORD     = 0x6005;
const REQUEST_FETCH_CACHE_SECONDS  = 0x6006;


/** Response */
const RESPONSE_FETCH_CLOSE       = 0x2002;
const RESPONSE_FETCH_ERROR       = 0x2003;
const RESPONSE_FETCH_PROGRESS    = 0x2004;
const RESPONSE_FETCH_WSERROR     = 0x2005;
const RESPONSE_FETCH_DONE        = 0x2006;
const RESPONSE_FETCH_READY       = 0x2007;
const REPONSE_SPEED_RATE         = 0x2008;

const ERR_WEBSOCKET_ERR          = 401;
const ERR_WEBSOCKET_CLOSED       = 402;
const ERR_WEBSOCKET_CONNECTING   = 403;
const ERR_WEBSOCKET_CONNECTED    = 404;

/**media_type */
const MEDIA_TYPE_VIDEO     = 0x1000;
const MEDIA_TYPE_AUDIO     = 0x1001;
const MEDIA_TYPE_LOCATION  = 0x1002;




/**cache 6 frames and write in file */
const batchWriteLength = 6

/**
 * only supports 16 websocket connections
 */

var clearData = function(){
    if(this.localFile == null)return;
    this.localFile.release();
    this.localFile = null;
    this.isInited = false;
}

var _release = function(){
    this.clearData();
    this.websocket && this.websocket.close();
    self.close();
    console.info("fetch.js release...")
}

var arrayBufferToString = function(buffer) {
    let uint8Array = new Uint8Array(buffer);
    let str = String.fromCharCode.apply(null, uint8Array);
    return str;
}

var arrayBufferToHex = function(buffer) {
    const bytes = new Uint8Array(buffer); // 按字节访问
    return Array.from(bytes)
      .map(byte => byte.toString(16).padStart(2, '0')) // 转成两位十六进制
      .join(' '); // 用空格分隔
  }


var _parseWebSocketMessage = function(dataView,data,frameType,length,n){
    var postData = null;
    if(this.needPassword && !this.isPasswordCorrected) return;
    switch(frameType){
        case FRAME_CONFIG:{//config frame
           var width = dataView.getInt32((++n) * 4,isLittleEndian);
           var height =  dataView.getInt32((++n) * 4,isLittleEndian);
           var audioType = dataView.getInt32((++n) * 4,isLittleEndian);//0:G711,1:g726,2:AAC,3:g711,
           var frameRate = dataView.getInt32((++n) * 4,isLittleEndian);
           var sampleRate = dataView.getInt32((++n) * 4,isLittleEndian);

           var bitWidthPerSample = dataView.getUint8((++n) * 4,isLittleEndian);

           var channelCount = dataView.getUint8(n * 4 + 1,isLittleEndian);
           var videoType = dataView.getUint8(n * 4 + 2,isLittleEndian);	//0:H264,1:H265,4:H265
           //tips : adding encrypted stream
           var reserved =  dataView.getUint8(n * 4 + 3,isLittleEndian);	//是否加密
           var passwordMd5 = "";//uppercase string
           if(reserved == 1){
               var tmpCache = data.slice(n * 4 + 3 + 5);
               passwordMd5 = arrayBufferToString(tmpCache);
           }
           console.info("isEncrypted=%d,pass=%s,video=%s,frameRate=%d,width=%d,height=%d,audioType=%s,videoType=%s,sampleRate=%d,bitWidthPerSample=%d,channelCount=%d",
           reserved,passwordMd5,videoType ,frameRate,width,height,audioType,videoType,sampleRate,bitWidthPerSample,channelCount);
           audioType = audioType == 0 ? 0 : 1;
           videoType = videoType == 0 ? 0 : 1;
           this.isAudioG711 = audioType == 0;
           console.info("audio=",audioType == 0 ? 'G711':'AAC',"video=",videoType == 0 ? "H264":"H265");
           this.isFirstVideoFrame = true;
           sampleRate = 8000;
           channelCount = 1;
           var isEncryptedStream = (reserved == 1);
           this.needVideoKeyFrame = true;
           if(isEncryptedStream){
                this.needPassword = isEncryptedStream;
                this.isPasswordCorrected = false;
           }else{
                this.needPassword = isEncryptedStream;
                this.isPasswordCorrected = false;
           }
          this.baseAudioPts = BigInt(-1);
          this.baseVideoPts = BigInt(-1);
          this.baseLocationPts = BigInt(-1);
          self.postMessage({
                type:REQUEST_NEW_DECODER,
                videoType:videoType,
                audioType:audioType,
                sampleRate:sampleRate,
                isEncryptedStream:isEncryptedStream,
                passwordMd5:passwordMd5,
                channelCount: channelCount
          });
          break;
        }
        case FRAME_IDR:{//IDR frame
           this.needVideoKeyFrame = false;
           var pts = dataView.getBigUint64((++n) * 4,isLittleEndian);
           pts = this._fixPTS(MEDIA_TYPE_VIDEO,pts);
           var result = data.slice(28);
           var index = VideoFrameMiddle;
           if(this.isFirstVideoFrame){
            this.isFirstVideoFrame = false;
           }
          if(this.isLive){
             postData = {
                type:REQUEST_FEED_DECODER_DATA,
                isAudio:false,
                data:result,
                pts:pts,
                index:index
             };
            self.postMessage(postData,[postData.data]);
          }else{
            this._writeLocalFile({
               isAudio:false,
               data:result,
               pts:pts,
               index:index
            });
            self.postMessage({
                type:RESPONSE_FETCH_PROGRESS,
                pts:pts
            });
           }
           //console.info("AAA Get IDR frame,totalSize = %d,size = %d,pts=%d",length,result.byteLength,pts)
           break;
        }
        case FRAME_PARTIAL:{//partial frame
           if(this.isFirstVideoFrame){
                return ;
           }
           if(this.needVideoKeyFrame) return;
           var pts = dataView. getBigUint64((++n) * 4,isLittleEndian);
            pts = this._fixPTS(MEDIA_TYPE_VIDEO,pts);
           var result = data.slice(28)
           var index = VideoFrameMiddle;
           if(this.isLive){
              postData = {
                type:REQUEST_FEED_DECODER_DATA,
                isAudio:false,
                data:result,
                pts:pts,
                index:index
             }
            self.postMessage(postData,[postData.data]);
          }
          else{
            this._writeLocalFile({
               isAudio:false,
               data:result,
               pts:pts,
               index:index
            });
            self.postMessage({
                type:RESPONSE_FETCH_PROGRESS,
                pts:pts
            });
          }
          this.cacheTotolTime = pts;
         
          //console.info("AAA Get partial frame,totalSize = %d,size = %d,pts=%d",length,result.byteLength,pts)
          break;
        }
        case FRAME_AUDIO:{//audio frame
           var pts = dataView. getBigUint64((++n) * 4,isLittleEndian);
           pts = this._fixPTS(MEDIA_TYPE_AUDIO,pts);
           var result = null
           if(this.isAudioG711){
             result = data.slice(28 + 4);
           }else{
             result = data.slice(28);
           }
           //var result = data.slice(28 + 4);//4byte haisi audio header
           var index = VideoFrameMiddle;

           if(this.isLive){
             postData = {
                type:REQUEST_FEED_DECODER_DATA,
                isAudio:true,
                data:result,
                pts:pts,
                index:index
             };
             self.postMessage(postData,[postData.data]);
           }else{
            self.postMessage({
                type:RESPONSE_FETCH_PROGRESS,
                pts:pts
            });
             this._writeLocalFile({
                isAudio:true,
                data:result,
                pts:pts,
                index:index
             });
           }
         
           //console.info("AAA Get audio frame,totalSize = %d,size = %d,pts=%d",length,result.byteLength,pts)
           break;
        }
        case FRAME_PARTIAL_IDR:
            this.needVideoKeyFrame = false;
            var result = this._composeMediaFrame(true,dataView,data);
            if(!result) return ;
            if(this.isFirstVideoFrame){
                this.isFirstVideoFrame = false;
            }
            var index = VideoFrameMiddle;
           if(this.isLive){
              postData = {
                 type:REQUEST_FEED_DECODER_DATA,
                 isAudio:false,
                 data:result["data"],
                 pts:result["pts"],
                 index:index
              };
             self.postMessage(postData,[postData.data]);
           }else{
             this._writeLocalFile({
                isAudio:false,
                data:result,
                pts:pts,
                index:index
             });
             self.postMessage({
                 type:RESPONSE_FETCH_PROGRESS,
                 pts:pts
             });
            }
            break;
        case FRAME_PARTIAL_P:
           if(this.isFirstVideoFrame || this.needVideoKeyFrame){
                return ;
           }
           var result = this._composeMediaFrame(false,dataView,data);
           if(!result) return ;
           var index = VideoFrameMiddle;
           if(this.isLive){
              postData = {
                type:REQUEST_FEED_DECODER_DATA,
                isAudio:false,
                data:result["data"],
                pts:result["pts"],
                index:index
             }
            self.postMessage(postData,[postData.data]);
          }
          else{
            this._writeLocalFile({
               isAudio:false,
               data:result,
               pts:pts,
               index:index
            });
            self.postMessage({
                type:RESPONSE_FETCH_PROGRESS,
                pts:pts
            });
          }
          this.cacheTotolTime = pts;
          break;
        case FRAME_GPS://GPS location
            var pts = dataView. getBigUint64((++n) * 4,isLittleEndian);
            pts = this._fixPTS(MEDIA_TYPE_LOCATION,pts);
            var gpsStatus = dataView.getInt8(64);
            if(gpsStatus == 65){
                n = 68;
                var latitudeDegree = dataView.getUint8(n);
                var latitudeCent = dataView.getUint8(++n);
                var longitudeDegree = dataView.getUint8(++n);
                var longitudeCent = dataView.getUint8(++n);
                var latitudeSec = dataView.getInt32(++n,true);
                n += 4;
                var longitudeSec = dataView.getInt32(n,true);
                var tmplongitude =  parseFloat(longitudeCent  / 60.00) * 10000000   + parseFloat(longitudeSec / 3600.00);
                var tmplatitude = parseFloat(latitudeCent  / 60.00) * 10000000  + parseFloat(latitudeSec / 3600.00);
                var longitude = parseFloat(longitudeDegree+"."+parseInt(tmplongitude));
                var latitude = parseFloat(latitudeDegree+"."+parseInt(tmplatitude));
                n = 88;
                const years = dataView.getUint16(n,isLittleEndian);
                n += 2;
                var  month = dataView.getUint8(n);
                var  days = dataView.getUint8(++n);
                var  hours = dataView.getUint8(++n);
                var  minutes = dataView.getUint8(++n);
                var  seconds = dataView.getUint8(++n);
                var timestamp = years + "-" + ((month >9) ? month: "0"+month) + '-' +((days >9) ? days: "0"+days) +
                    " " + ((hours >9) ? hours: "0"+hours) + ":" + ((minutes >9) ? minutes: "0"+minutes) + ":" + ((seconds >9) ? seconds: "0"+seconds)
                console.info("GPS",pts,timestamp,latitude,longitude);
                var postData = {
                    type:REQUEST_FEED_GPS_DATA,
                    pts:pts,             //playtime
                    timestamp:timestamp, //createTime
                    latitude:latitude,
                    longitude:longitude
                };
                self.postMessage(postData);
            }
            break;

        default:{
          console.error("not implemented frameType found")
          break;
        }
    }
}

/**
 * Several partial I/P video frames compose to  one complete video frame 
 * @param {*} mediaObject 
 */
var _composeMediaFrame = function(isKeyFrame,dataView,data){
    //this.partialVideoBuffer = null;
    //this.partialVideoSeq = 0;
    //this.partialVideoSum = 0;
    //this.partialVideoIsKeyFrame = 0;
    var isLittleEndian = true;
    var postion = 20;
    var seq = dataView.getUint16(postion,isLittleEndian);
    var sum = dataView.getUint16(postion + 2,isLittleEndian);
    var frameSize = dataView.getInt32(postion + 4,isLittleEndian);
    var pts = -1;

    var buffer = null;
    if(seq == 1){
        pts = dataView.getBigUint64(postion + 8,isLittleEndian);
       //console.log(`PTSXXXXX=0x${pts.toString(16)}`);
       //arrayBufferToHex(data.slice(28,8));
       //console.info("pts =====",pts);
        pts = this._fixPTS(MEDIA_TYPE_VIDEO,pts);
        this.partialVideoPts = pts;
        buffer = data.slice(28 + 8);
    }else{
        buffer = data.slice(28);
    }
    //console.info("[CVF0]buffer=",buffer.byteLength,"==",frameSize,"isKeyFrame=",isKeyFrame,"seq=",seq,"sum=",sum,"pts=",this.partialVideoPts);
    var targetBuffer = null;
    if(!isKeyFrame && this.needVideoKeyFrame){
        console.log("can't found video key frame, ignore partial video frames");
        return null;
    }
    if(seq == 1){
            this.partialVideoBuffer = new Uint8Array(buffer);
            this.partialVideoSeq = 1;
            this.partialVideoIsKeyFrame = isKeyFrame;
            this.partialVideoSum = sum;
            this.partialVideoPts = pts;
    }else if(seq > 1){
        this.partialVideoSeq += 1;
        if(seq != this.partialVideoSeq ||
                this.partialVideoSum != sum ||
                this.partialVideoIsKeyFrame != isKeyFrame ||
                this.partialVideoBuffer == null){
                console.info("partial video frame parse failed,seq("+seq+")/sum("+sum+")/isKeyFrame("+isKeyFrame+")/partialVideoBuffer is not correct");
                return null;
        }
    }else{
        console.info("partial video frame parse failed,seq can not be lower than 0");
        return null;
    }
    if(seq == sum){
        if(isKeyFrame){
            this.needVideoKeyFrame = false;
        }
        if(seq == 1){
           targetBuffer = this.partialVideoBuffer.buffer;
        }else{
           targetBuffer = this._mergeMediaFrameBuffer(buffer);
        }
        const mediaObject =  {data:targetBuffer, pts:this.partialVideoPts};
        this._resetMediaFrameBuffer();
        //console.info("[CVF1]targetBuffer=",targetBuffer.byteLength,"isKeyFrame=",isKeyFrame,"seq=",seq,"sum=",sum,"pts=",mediaObject);
        return mediaObject;
    }else{
        if(seq != 1){
            this._mergeMediaFrameBuffer(buffer);
        }
        return null;
    }
    
}

var _resetMediaFrameBuffer = function(){
    this.partialVideoBuffer = null;    
    this.partialVideoSeq = -1;
    this.partialVideoIsKeyFrame = false;
    this.partialVideoSum = -1;
    this.partialVideoPts = -1;
}

var _mergeMediaFrameBuffer = function(buffer){
    const offset1 = this.partialVideoBuffer.buffer.byteLength;
    const array1 = new Uint8Array(offset1 + buffer.byteLength);
    array1.set(this.partialVideoBuffer,0);
    array1.set(new Int8Array(buffer),offset1);
    this.partialVideoBuffer = array1;
    return array1.buffer;
}



var _resetSpeedRate = function(){
    this.lastSpeedTimestamp = 0;
    this.lastSpeedSize = 0;
    const postData = {
        type:REPONSE_SPEED_RATE,
        speedRate: "0kbps",
        timestamp:0
     };
     self.postMessage(postData);
}

var onWebSocketError = function(event){
    console.info("onWebsocketError");
    this.errno = ERR_WEBSOCKET_ERR;
    this._resetSpeedRate();
    
}
var onWebSocketClosed = function(){
    console.info("onWebsocketClosed");
    this.errno = ERR_WEBSOCKET_CLOSED;
    this._resetSpeedRate();
}
var onWebSocketOpened = function(event){
    console.info("fetch onwebSocketopened");
    this.lastDataReceivedTime = this.getNowTime();
    this.errno = ERR_WEBSOCKET_CONNECTED;
    //this.tryRetryTimes = 0;
    this.websocket.send(this.playParams);
}

var onWebSocketMessage = function(event){
    var data = event.data;
    if(data == "") return ;
    var timestamp = this.getNowTime();
    if(!this.noSpeedRate) {
        this.calculateSpeedRate(data.byteLength,timestamp);
    }
    this.lastDataReceivedTime = timestamp;
    var dataView = new DataView(data);
    var n = 0;
    var type = dataView.getInt32(n,isLittleEndian);
    var length = dataView.getInt32((++n) * 4,isLittleEndian);
    var errCode = dataView.getInt32((++n) * 4,isLittleEndian);
    var frameType = dataView.getInt32((++n) * 4,isLittleEndian);
    var param2 = dataView.getInt32((++n) * 4,isLittleEndian);

    //tips : adding encrypted stream
    //console.info("type=",type,"length=",length,"errCode=",errCode,"frameType=",frameType,"param2=",param2);

    //has an error
    if(type == 481){
        if(frameType == EventRecordDownloadCompleted){
            this.noSpeedRate = true;
            this.lastSpeedRate = 0;
            this.onRecordDownloadCompleted();
            this._resetSpeedRate();
        }else{
            this.errno = ERR_WEBSOCKET_ERR;
            this._resetSpeedRate();
            /*self.postMessage({
                type:RESPONSE_FETCH_ERROR,
                errno:frameType
            });*/
            //this._requestStreamFailed("the device is busy or file is not existed. errno is " + frameType);
        }
    }else{
        if(data.byteLength >= length){
            this.parseWebSocketMessage(dataView,data,frameType,length,n);
        }
    }
}



var _readLocalFile = function(){
    if(this.localFile == null){
        console.info("_readLocalFile failed for localFile is null");
        return;
    }
    var n = 1;
    /*do{
        if(this.readOffset >= this.writeOffset){
            //console.info("this.readOffset (%d) >= this.writeOffset (%d)",this.readOffset,this.writeOffset);
            break;
        }
        this.localFile.read(this.readOffset++);
    }while(n--);*/
    do{
        this.localFile.read();
    }while(n--);
}

var _writeLocalFile = function(obj,forceWrite){
    if(this.localFile == null){
        console.info("_writeLocalFile failed for localFile is null");
        return 0;
    }
    this.batchWriteBuffer.push({
        id:     0,
        isAudio:obj.isAudio,
        pts:    obj.pts,
        index:  obj.index,
        data:   obj.data
    });

    if(this.batchWriteBuffer.length >= batchWriteLength || forceWrite){
        var copies = this.batchWriteBuffer;
        this.batchWriteBuffer = [];
        this.localFile.batchWrite(copies);
    }

    /*this.localFile.write({
        id:     this.writeOffset++,
        isAudio:obj.isAudio,
        pts:    obj.pts,
        index:  obj.index,
        data:   obj.data
    });*/
    //console.info("_writeLocalFile isAudio=",obj.isAudio);
}


var _onRecordDownloadCompleted = function(){
    this.websocket && this.websocket.close();
    this.websocket = null;
    this.isHistoryStreamTransformDone  = true;
    if(!this.isLive){
        var forceWrite = true;
        this._writeLocalFile({
            isAudio:false,
            data:new ArrayBuffer(1),
            pts:this.duration,
            index:VideoFrameLast
         },forceWrite);
        self.postMessage({
            type:RESPONSE_FETCH_DONE,
        });
    }
}


var startWebsocket = function(){
    console.info("_startWebsocket",this.url);
    this.websocket = new WebSocket(this.url);
    this.websocket.binaryType = 'arraybuffer';
    this.websocket.onmessage = onWebSocketMessage.bind(this);
    this.websocket.onclose = onWebSocketClosed.bind(this);
    this.websocket.onerror = onWebSocketError.bind(this);
    this.websocket.onopen = onWebSocketOpened.bind(this);
    this.errno = ERR_WEBSOCKET_CONNECTING;
}


var _initLocalFile = function(){
    if(this.isLive) return ;
    // const fileName = Math.random().toString().substring(2)CSeq 变化触发新 Transaction	当客户端发送新 CSeq 的请求时，通常会产生新的 Transaction
    this.localFile = new self.DB4({callback:this});
 }

 var _closeLocalFile = function(){
    if(this.localFile !=null){
        this.localFile.release();
        this.readOffset = 0;
        this.writeOffset = 0;
        this.localFile = null;
    }
}


var _fixPTS = function(mediaType,pts){
    switch(mediaType){
        case MEDIA_TYPE_VIDEO:
            if(this.baseVideoPts == BigInt(-1)){
                this.baseVideoPts = pts;
            }
            return Number(pts - this.baseVideoPts);
        case MEDIA_TYPE_AUDIO:
            if(this.baseAudioPts == BigInt(-1)){
                this.baseAudioPts = pts;
            }
            return Number(pts - this.baseAudioPts);
        case MEDIA_TYPE_LOCATION:
            if(this.baseLocationPts == BigInt(-1)){
                this.baseLocationPts = pts;
            }
            return Number(pts - this.baseLocationPts);
        default:
            console.info("unknown media type");
            return 0;
    }
}

var _cleanSocket = function(){
    console.info("_cleanSocket",this.url);
    if(this.websocket){
        this.websocket.onmessage = null
        this.websocket.onclose = null
        this.websocket.onerror = null
        this.websocket.onopen = null
        this.websocket && this.websocket.close()
        this.websocket = null;
    }
}

var _calculateSpeedRate = function(byteLength,timestamp){
    if(this.lastSpeedTimestamp == 0) {
        this.lastSpeedTimestamp = timestamp;
    }
    const timediff = timestamp - this.lastSpeedTimestamp;
    this.lastSpeedSize += byteLength;
    if(timediff >= 1000){
       const speed = this.lastSpeedSize * 8 / timediff ;
       this.lastSpeedTimestamp = 0;
       this.lastSpeedSize = 0;
       var postData = {
        type:REPONSE_SPEED_RATE,
        speedRate: parseInt(speed) + "kbps",
        timestamp:timestamp
       };
     self.postMessage(postData);
    }
}


var _reConnection = function(isError){
    var self1 = this;
    self1.cleanSocket();
    //history data is transform over
    console.log("reConnection",
        "isError="+isError,
        "isHistoryStreamTransformDone="+self1.isHistoryStreamTransformDone,
        "tryRetryTimes="+self1.tryRetryTimes,
        "retryTimes="+self1.retryTimes,
        "retryDuration="+self1.retryDuration,
    );
    if(self1.isHistoryStreamTransformDone) return ;

    if(self1.retryTimes == -1 || self1.tryRetryTimes++ < self1.retryTimes){
            self1.closeLocalFile();
            self1.initLocalFile();
            self1.startWebsocket();
    }else{
        self1.stopTimer();
        if(isError){
            self.postMessage({
                type:RESPONSE_FETCH_ERROR,
            });
        }else{
            self.postMessage({
                type:RESPONSE_FETCH_CLOSE,
            });
        }
    }
}

var _getNowTime = function(){
    return Date.now();
}

var _loopRoutine = function(){
    
    /*console.info("network connection checked",
                 "errno="+this.errno,
                 "nowTime="+this.getNowTime(),
                 "lastDataReceviedTime="+this.lastDataReceivedTime,
                 "maxDataReceivedTime="+this.maxDataReceivedTime
    );*/
    switch(this.errno){
        case ERR_WEBSOCKET_CONNECTING:
            console.info("ERR_WEBSOCKET_CONNECTING");
            this._resetSpeedRate();
            return ;
        case ERR_WEBSOCKET_CLOSED:
            this._resetSpeedRate();
            this.reConnection(false);
            return;
        case ERR_WEBSOCKET_ERR:
            this._resetSpeedRate();
            this.reConnection(true);
            return;
        case ERR_WEBSOCKET_CONNECTED:
            if(this.getNowTime() - this.lastDataReceivedTime > this.maxDataReceivedTime){
                this.errno = ERR_WEBSOCKET_CLOSED;
                this._resetSpeedRate();
                this.reConnection(false);
            }
            return;
    }
}
var _startTimer = function(){
    var self = this;
    self.timer = setInterval(function(){
        self.loopRoutine();
    },self.retryDuration);
}
var _stopTimer = function(){
    if(this.timer!=null){
        clearInterval(this.timer);
        this.timer = null;
    }
}


var fetcher = function(option){
    this.url = option.url;
    this.isLive = option.isLive;
    this.duration = option.duration;
    this.playParams = option.playParams;

    this.errno = 0;
    this.tryRetryTimes = 0;
    this.retryTimes =  option.retryTimes;
    this.retryDuration = option.retryDuration;

    this.isHistoryStreamTransformDone = false;

    this.websocket = null;
    this.writeOffset = 0;
    this.readOffset = 0;
    this.localFile = null;
    this.beginTimeOffset = -1;
    this.isInited = false;
    this.isFirstVideoFrame = false;
    //18 seconds
    this.maxDataReceivedTime = 18000;

    this.batchWriteBuffer = [];

    this.needPassword = false;
    this.isPasswordCorrected = false;
    this.needVideoKeyFrame = false;
    //bitRate
    this.lastSpeedTimestamp = 0;
    this.lastSpeedSize = 0;
    this.lastSpeedRate = 0;
    //video file is download completed
    this.noSpeedRate = false;
    this.isAudioG711 = false;

    this.baseAudioPts = BigInt(-1);
    this.baseVideoPts = BigInt(-1);
    this.baseLocationPts = BigInt(-1);

    this._resetMediaFrameBuffer();
    
    this.startWebsocket();
    this.initLocalFile();
    this.lastDataReceivedTime = _getNowTime();
    this.startTimer();
};

var _FN = fetcher.prototype;

var _attributes = {
    /** start websocket */
    startWebsocket:startWebsocket,
    onWebSocketError:onWebSocketError,
    onWebSocketClosed:onWebSocketClosed,
    onWebSocketOpened:onWebSocketOpened,
    onWebSocketMessage:onWebSocketMessage,
    parseWebSocketMessage:_parseWebSocketMessage,
    cleanSocket:_cleanSocket,
    /** end websocket */
    clearData:clearData,
    release:_release,
    calculateSpeedRate:_calculateSpeedRate,
    onRecordDownloadCompleted:_onRecordDownloadCompleted,
    /**local storage */
    initLocalFile:_initLocalFile,
    reConnection:_reConnection,
    getNowTime:_getNowTime,
    startTimer:_startTimer,
    stopTimer:_stopTimer,
    loopRoutine:_loopRoutine,
    _composeMediaFrame:_composeMediaFrame,
    _mergeMediaFrameBuffer:_mergeMediaFrameBuffer,
    _readLocalFile:_readLocalFile,
    _resetMediaFrameBuffer:_resetMediaFrameBuffer,
    _writeLocalFile:_writeLocalFile,
    _resetSpeedRate:_resetSpeedRate,
    closeLocalFile:_closeLocalFile,
    _fixPTS:_fixPTS,
    /***begin for fs.js */
    onInitSuccess:function(e){
        console.info("onFsInitSuccess",e)
     },
     onInitFailed:function(e){
         console.info("onFsInitFailed",e)
     },
     onWriteSuccess:function(e){
       //console.info("onWriteSuccess",e)
     },
     onWriteFailed:function(e){
         console.info("onFsWriteFailed",e)
     },
     onReadSuccess:function(data){
        if(data == null){ return ;}
        var postData = {
            type:REQUEST_FEED_DECODER_DATA,
            isAudio:data.isAudio,
            data:data.data,
            pts:data.pts,
            index:data.index
         };
         self.postMessage(postData,[postData.data]);
     },
     onReadFailed:function(e){
         console.info("onReadFailed",e)
     },
     onDeleteSuccess:function(e){
         console.info("onDeleteSuccess",e)
     },
     onDeleteFailed:function(e){
         console.info("onDeleteFailed",e)
     }

};

for(var key in _attributes){
   _FN[key] =  _attributes[key];
}

var FR = null;
self.onmessage = function (res){
    data = res.data;
    switch(data.type){
       case REQUEST_FETCH_START:
            FR = new fetcher(data.option);
            break;
       case REQUEST_FETCH_PASSWORD:
            FR.isPasswordCorrected = true;
            break;
       case REQUEST_FETCH_RELEASE:
            if(FR == null)return ;
            FR.release();
            FR = null;
            break;
       case REQUEST_FETCH_FRAMES:
            FR._readLocalFile();
            break;
    }
};

self.postMessage({
    type:RESPONSE_FETCH_READY
});
